﻿//-----------------------------------------------------------------------
// <copyright file="InstallationProcess.cs" author="Artak Mkrtchyan">
//     Creative Commons - GNU General Public License (http://creativecommons.org/licenses/GPL/2.0/
// </copyright>
// <author>Artak Mkrtchyan</author>
// <email>mkArtak@gmail.com</email>
// <date>20/03/2013</date>

using ConsoleHoster.Common.Utilities;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace AppUpdateInstaller.Model
{
	public delegate void InstallationProgressEventHandler(InstallationProcess argSender, InstallationProgressEventArgs argEA);

	public sealed class InstallationProcess
	{
		public event InstallationProgressEventHandler StageStarting;
		public event InstallationProgressEventHandler StageFinished;
		public event InstallationProgressEventHandler InstallationFailure;
		public event InstallationProgressEventHandler ProgressChange;

		private readonly LinkedList<IInstallationStage> stages = new LinkedList<IInstallationStage>();
		private int stagesPassed = 0;
		private bool installationStarted = false;
		private readonly object installationProcessSyncer = new object();
		private readonly ILogger logger;

		public InstallationProcess(ILogger argLogger)
		{
			this.logger = new ComponentLogger("InstallationProcess", argLogger);
		}

		public void AddInstallationStage(IInstallationStage argStage)
		{
			this.ValidateInstallationStage();

			if (argStage == null)
			{
				throw new ArgumentNullException("argStage");
			}

			lock (this.installationProcessSyncer)
			{
				this.ValidateInstallationStage();

				this.stages.AddLast(argStage);
			}
		}

		public async Task RunAsync()
		{
			if (this.installationStarted)
			{
				return;
			}

			lock (this.installationProcessSyncer)
			{
				this.installationStarted = true;
			}

			this.logger.LogMessage("Starting installation process...");
			Stack<IInstallationStage> tmpPassedStages = new Stack<IInstallationStage>(this.stages.Count);
			foreach (IInstallationStage tmpStage in this.stages)
			{
				this.NotifyStageStarting(tmpStage);
				if (!await this.TryRunStageAsync(tmpStage))
				{
					this.logger.LogWarning("Starting to roll-back already passed stages");
					foreach (IInstallationStage tmpCompletedStage in tmpPassedStages.Where(item => item.SupportsRollback))
					{
						this.logger.LogWarning("Trying to roll-back stage: '{0}'", tmpCompletedStage.ToString());

						if (!this.TryRollbackStage(tmpCompletedStage))
						{
							break;
						}
					}

					this.logger.LogWarning("Installation failed");
					this.NotifyInstallationFailure(tmpStage);
					break;
				}

				this.stagesPassed++;

				this.logger.LogMessage("Installation stage '{0}' has passed successfully", tmpStage.ToString());
				this.NotifyStageFinished(tmpStage);
			}

			this.FinalizeInstallation();
		}

		private void FinalizeInstallation()
		{
			foreach (IInstallationStage tmpStage in this.stages)
			{
				if (tmpStage.HasFinalizerStage)
				{
					tmpStage.FinalizeStage();
				}
			}
		}

		private async Task<bool> TryRunStageAsync(IInstallationStage tmpStage)
		{
			bool tmpResult = false;
			tmpStage.ProgressChanged += this.NotifyProgressChange;
			try
			{
				this.logger.LogMessage("Starting installation stage: '{0}'", tmpStage.ToString());
				await tmpStage.RunAsync();
				tmpResult = true;
			}
			catch (Exception ex)
			{
				this.logger.LogError("Installation stage: '{0}' has failed with reason: '{1}'", tmpStage.ToString(), ex.ToString());
				this.TryRollbackStage(tmpStage);
			}
			finally
			{
				tmpStage.ProgressChanged -= this.NotifyProgressChange;
			}

			return tmpResult;
		}

		private bool TryRollbackStage(IInstallationStage tmpStage)
		{
			bool tmpResult = true;
			if (tmpStage.SupportsRollback)
			{
				try
				{
					this.logger.LogWarning("Trying to rollback failed installation stage: {0}", tmpStage.ToString());
					tmpStage.RollBack();
				}
				catch (Exception ex2)
				{
					tmpResult = false;
					this.logger.LogWarning("Rollback of the faild installation stage: '{0}' has failed, with exception: {1}", tmpStage.ToString(), ex2.ToString());
				}
			}

			return tmpResult;
		}

		private void ValidateInstallationStage()
		{
			if (this.installationStarted)
			{
				throw new ApplicationException("Installation have been started. No changes to the installation stages are allowed at this point");
			}
		}

		private void NotifyStageStarting(IInstallationStage argStage)
		{
			InstallationProgressEventHandler temp = this.StageStarting;
			if (temp != null)
			{
				temp(this, new InstallationProgressEventArgs(argStage, 100 * this.stagesPassed / this.stages.Count));
			}
		}

		private void NotifyInstallationFailure(IInstallationStage argStage)
		{
			InstallationProgressEventHandler temp = this.InstallationFailure;
			if (temp != null)
			{
				temp(this, new InstallationProgressEventArgs(argStage, 0));
			}
		}

		private void NotifyStageFinished(IInstallationStage argStage)
		{
			InstallationProgressEventHandler temp = this.StageFinished;
			if (temp != null)
			{
				temp(this, new InstallationProgressEventArgs(argStage, this.GetCurrentProgress()));
			}
		}

		private void NotifyProgressChange(IInstallationStage argStage, InstallationStageProgressEventArgs argEA)
		{
			InstallationProgressEventHandler temp = this.ProgressChange;
			if (temp != null)
			{
				temp(this, new InstallationProgressEventArgs(argStage, this.GetCurrentProgress(), argEA.Status));
			}
		}

		private int GetCurrentProgress()
		{
			return 100 * this.stagesPassed / this.stages.Count;
		}
	}
}
